/**
 * Copyright © 2011 Creativor Studio.
 * About license information, please see LICENSE.txt.
 */
package org.creativor.rayson.util;

import java.lang.reflect.GenericArrayType;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import org.creativor.rayson.annotation.Protocols;
import org.creativor.rayson.api.AsyncRpcProtocol;
import org.creativor.rayson.api.CallFuture;
import org.creativor.rayson.api.RpcProtocol;
import org.creativor.rayson.api.Session;
import org.creativor.rayson.common.Stream;
import org.creativor.rayson.exception.IllegalMethodException;
import org.creativor.rayson.exception.IllegalServiceException;
import org.creativor.rayson.exception.NetWorkException;
import org.creativor.rayson.exception.RpcException;

// TODO: Auto-generated Javadoc
/**
 * This tool is used to verify the {@link RpcProxy}.
 * 
 * @author Nick Zhang
 */
public final class ProxyVerifier
{
	private ProxyVerifier()
	{
		
	}
	
	/**
	 * Verify proxy method.
	 * 
	 * @param method
	 *            the method
	 * @throws IllegalMethodException
	 *             the illegal method exception
	 */
	public static void verifyProxyMethod(Method method) throws IllegalMethodException
	{
		// 1. must throws rpc-exception.
		boolean foundRemoteException = false;
		for (Class exceptionType : method.getExceptionTypes())
		{
			if (exceptionType == RpcException.class)
			{
				foundRemoteException = true;
				break;
			}
		}
		if (!foundRemoteException)
			throw new IllegalMethodException(method, "Must throws " + RpcException.class.getName());
		
		// 2. return type must be portable.
		Class returnType = method.getReturnType();
		if (!Stream.isPortable(returnType))
			throw new IllegalMethodException(method, "Return type must be portable");
		boolean foundSessionPara = false;
		// 3. every parameter type must be portable.
		Class[] parameterTypes = method.getParameterTypes();
		for (Class type : parameterTypes)
		{
			if (Session.class.isAssignableFrom(type)) foundSessionPara = true;
			if (!Stream.isPortable(type))
				throw new IllegalMethodException(method, "Parameter type must be portable");
		}
		// 4. must not session paramter type.
		if (foundSessionPara)
			throw new IllegalMethodException(method, "Parameter type must not "
					+ Session.class.getName());
	}
	
	/**
	 * Verify async proxy method.
	 * 
	 * @param method
	 *            The async proxy method to be verified.
	 * @throws IllegalMethodException
	 *             If the proxy method is not a legal one.
	 */
	public static void verifyAsyncProxyMethod(Method method) throws IllegalMethodException
	{
		// 1. must throws only network-exceptoion.
		Class<?>[] exceptionTypes = method.getExceptionTypes();
		if (exceptionTypes.length != 1
				|| !NetWorkException.class.isAssignableFrom(exceptionTypes[0]))
		{
			throw new IllegalMethodException(method, "Should only throws "
					+ NetWorkException.class.getName());
		}
		
		// 2. return type must be call future.
		Class returnType = method.getReturnType();
		if (!CallFuture.class.isAssignableFrom(returnType))
			throw new IllegalMethodException(method, "Return type must be "
					+ CallFuture.class.getName());
		// 3. call future parameter type must be portable.
		ParameterizedType genericReturnType = (ParameterizedType) method.getGenericReturnType();
		Type[] actualTypeArguments = genericReturnType.getActualTypeArguments();
		
		Type genericType = actualTypeArguments[0];
		
		if (GenericArrayType.class.isAssignableFrom(genericType.getClass()))
			genericType = ((GenericArrayType) genericType).getGenericComponentType();
		
		if (!Stream.isPortable((Class) genericType))
			throw new IllegalMethodException(method, "Return type must be portable");
		boolean foundSessionPara = false;
		// 4. every parameter type must be portable.
		Class[] parameterTypes = method.getParameterTypes();
		for (Class type : parameterTypes)
		{
			if (Session.class.isAssignableFrom(type)) foundSessionPara = true;
			if (!Stream.isPortable(type))
				throw new IllegalMethodException(method, "Parameter type must be portable");
		}
		// 5. must not session paramter type.
		if (foundSessionPara)
			throw new IllegalMethodException(method, "Parameter type must not "
					+ Session.class.getName());
		
	}
	
	/**
	 * Verify async proxy method.
	 * 
	 * @param method
	 *            The async proxy method to be verified.
	 * @param protocolMethod
	 *            Associated method of {@link RpcProtocol}.
	 * @throws IllegalMethodException
	 *             If the proxy method is not a legal one.
	 */
	public static void verifyAsyncProxyMethod(Method method, Method protocolMethod)
			throws IllegalMethodException
	{
		// verify return type match.
		ParameterizedType genericReturnType = (ParameterizedType) method.getGenericReturnType();
		Type[] typeArguments = genericReturnType.getActualTypeArguments();
		if (typeArguments.length != 1)
			throw new IllegalMethodException(method,
					"No type argument found in generic return type: " + genericReturnType);
		Class returnType = protocolMethod.getReturnType();
		Type typeArgument = typeArguments[0];
		
		if (returnType.isArray())
		{
			returnType = returnType.getComponentType();
			GenericArrayType arrayTypeArgument = (GenericArrayType) typeArgument;
			typeArgument = arrayTypeArgument.getGenericComponentType();
		} else
		{
			returnType = Reflection.getNonePrimitiveClass(returnType);
		}
		if (returnType != typeArgument)
			throw new IllegalMethodException(method, "Return type argument " + typeArgument
					+ " is not match to protocol method return type:" + returnType.getName());
		
		verifyAsyncProxyMethod(method);
	}
	
	/**
	 * Verify {@link AsyncRpcProtocol}.
	 * 
	 * @param asyncProxy
	 *            {@link AsyncRpcProtocol} to be verified.
	 * @throws IllegalServiceException
	 *             If the {@link AsyncRpcProtocol} is not an legal one.
	 */
	public static void verifyAsyncProxy(Class<? extends AsyncRpcProtocol> asyncProxy)
			throws IllegalServiceException
	{
		// verify rpc proxy interface.
		if (!asyncProxy.isInterface())
			throw new IllegalServiceException("Proxy class " + asyncProxy.getName()
					+ " must be an interface");
		
		Protocols protocols = asyncProxy.getAnnotation(Protocols.class);
		if (protocols == null)
			throw new IllegalServiceException("Must has annotaion " + Protocols.class.getName());
		
		Class<? extends RpcProtocol>[] protocolClasses = protocols.value();
		
		Method matchedMethod;
		
		// verify every method.
		for (Method proxyMethod : asyncProxy.getMethods())
		{
			// Ignore getSession method.
			if (proxyMethod.getName().equals("getSession")
					&& proxyMethod.getParameterTypes().length == 0) continue;
			matchedMethod = null;
			for (Class<? extends RpcProtocol> protocolClass : protocolClasses)
			{
				try
				{
					matchedMethod =
							protocolClass.getMethod(proxyMethod.getName(),
									proxyMethod.getParameterTypes());
					break;
				} catch (Exception e)
				{
					// Ignore this exception.
				}
			}
			if (matchedMethod == null)
				throw new IllegalServiceException("Can not find matched method of proxy method:"
						+ proxyMethod.toString() + " in RPC protocols");
			try
			{
				ProxyVerifier.verifyAsyncProxyMethod(proxyMethod, matchedMethod);
			} catch (IllegalMethodException e)
			{
				throw new IllegalServiceException(e.getMessage());
			}
		}
		
	}
}
